# -*- coding: UTF-8 -*-
# File date: Hi_2022/12/12 21:45
# File_name: balloon_tips.py
from tkinter import Label, Toplevel
from typing import Literal


class Balloon:
    def __init__(self,
                 widget,
                 text: str = "",
                 fg="black",
                 bg="SkyBlue",
                 justify: Literal["left", "center", "right"] = "left",
                 anchor: Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] = "center",
                 x_offset: int = 0,
                 y_offset: int = 0,
                 show_width: int = 0,
                 follow: bool = True,
                 alpha: float = 0.95,
                 delayed=250,
                 topmost=True,
                 *args,
                 **kwargs):
        """
        导入Balloon类即可使用,传入master参数,建议为根窗口。
            :param widget: 要提示的组件
            :param text: 提示的文字信息
            :param fg: 前景色
            :param bg:背景色
            :param justify:定义对齐方式,可选Literal["left", "center", "right"],默认为 "left"。
            :param anchor:文本或图像在背景内容区的位置,默认"w",可选Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"]
            :param x_offset: 显示提示左上角x坐标偏移量,>0向右,<0向左,默认0
            :param y_offset: 显示提示左上角y坐标偏移量,>0向下,默认有15的偏移量必须有偏移量否则当与鼠标处于同一位置会造成无法显示的问题
            :param show_width: 提示框宽度,以像素为单位,不更改字体缩放比例100%大概12个像素一个汉字
            :param alpha: 透明度,1.0表示不透明,0.0表示完全透明
            :param follow: 提示跟随移动,设置Ture时鼠标在指定组件内移动时会一直跟随,False则只会在进入时显示就不再跟随显示
            :param topmost: 窗口置顶,在主窗口已经置顶时需要添加此参数
            :param args: 其他更多参数参考tkinter.Label可传入参数
            :param kwargs: 其他更多参数参考Label可传入参数
        """
        self.text = text
        self.fg = fg
        self.bg = bg
        self.width = show_width
        self.justify = justify
        self.anchor = anchor
        self.args = args
        self.kwargs = kwargs

        self.top = None
        self.id = None
        self.follow = follow
        self.widget = widget
        self.alpha = alpha
        self.delayed = delayed  # 延时,毫秒
        self.topmost = topmost

        # xy坐标偏移量
        self.x_offset = x_offset
        self.y_offset = y_offset

        self.widget.bind('<Enter>', lambda x: self.enter())  # 进入控件区域
        self.widget.bind("<ButtonPress>", lambda x: self.leave())  # 按下任意鼠标按下消失
        self.widget.bind('<Leave>', lambda x: self.leave())  # 离开控件区域
        if self.follow:  # 跟随显示
            self.widget.bind("<Motion>", lambda x: self.move())  # 鼠标移动提示跟随

    def balloon_show(self):
        """显示toplevel"""
        if self.top:
            return

        self.updata_width()  # 显示前先更新宽度

        self.top = Toplevel()
        self.top.attributes("-alpha", self.alpha)  # 透明度
        self.top.overrideredirect(True)  # 只保留窗口不要最小化关闭等
        self.top.attributes("-topmost", self.topmost)  # 置顶
        self.top.minsize(self.width, 30)  # 宽度最少需要240小于240不能居中

        # 偏移量默认居中
        x = self.widget.winfo_pointerx() + self.x_offset - self.width // 2
        if self.y_offset + 15 < 15:
            self.y_offset = 0
        y = self.widget.winfo_pointery() + self.y_offset + 15
        self.top.geometry(f"+{x}+{y}")

        balloon = Label(self.top,
                        text=self.text,
                        fg=self.fg,
                        bg=self.bg,
                        justify=self.justify,
                        anchor=self.anchor,
                        wraplength=self.width - 20,
                        *self.args, **self.kwargs)
        balloon.pack(fill="both", expand=1, ipadx=10, ipady=5)  # ipadx要等于wraplength的一半

    def updata_width(self):
        """根据文本大小自适应宽度"""
        if not self.width:
            test_long = len(self.text)
            if test_long <= 30:
                self.width = test_long * 12 + 20  # 每个字大概12,20是给宽度补充的
            elif test_long <= 90:
                self.width = 300
            elif 90 < test_long <= 250:
                self.width = test_long + 200
            else:
                self.width = 450

    def enter(self):
        self.schedule()

    def leave(self):
        self.unschedule()
        self.destroy_balloon()

    def schedule(self):
        self.unschedule()
        self.id = self.widget.after(self.delayed, self.balloon_show)

    def unschedule(self):
        show_id = self.id
        self.id = None
        if show_id:
            self.widget.after_cancel(show_id)

    def move(self):
        """移动提示框"""
        if self.top:
            x = self.widget.winfo_pointerx() + self.x_offset - self.width // 2
            if self.y_offset + 15 < 15:
                self.y_offset = 0
            y = self.widget.winfo_pointery() + self.y_offset + 15
            self.top.geometry(f"+{x}+{y}")

    def destroy_balloon(self):
        """取消气泡提示"""
        if self.top:
            self.top.destroy()
            self.top = None

    def updata_text(self, new_text, new_width=0):
        """
        更新文本并重新计算宽
        :param new_text: 要更新的文本
        :param new_width: 重置的宽度不设置默认0,则自动计算新宽度
        """
        self.text = new_text
        self.width = new_width  # 设置False才能重新计算高度

    def unballoon(self):
        """
        彻底取消气泡,再次使用需要重新调用Balloon
        重复调用会报AttributeError: 'Balloon' object has no attribute 'widget'
        """
        try:
            self.widget.unbind('<Enter>')  # 进入控件区域
            self.widget.unbind("<ButtonPress>")  # 按下任意鼠标按下消失
            self.widget.unbind('<Leave>')  # 离开控件区域
            if self.follow:  # 跟随显示
                self.widget.unbind("<Motion>")  # 鼠标移动提示跟随

            del self.text
            del self.fg
            del self.bg
            del self.justify
            del self.anchor
            del self.args
            del self.kwargs

            del self.top
            del self.id
            del self.follow
            del self.widget
            del self.width
            del self.alpha
            del self.delayed
            del self.topmost

            # xy坐标偏移量
            del self.x_offset
            del self.y_offset
            return True
        except AttributeError:
            return 1
